home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
L' Effet Pommier 3
/
L'Effet Pommier - Volume 03.iso
/
Programmation
/
gray image 2.1
/
README
< prev
next >
Wrap
Text File
|
1995-07-30
|
16KB
|
397 lines
Gray-scale image processing library
***** For the version history, read on
***** You'll need libserv.a, an "advanced" C++ iostream classlib,
to compile and use this grayimage library
That iostream code has been posted on comp.sources.misc and info-mac
(info-mac:/dev/lib/advanced-io-cpp.hqx) and is also available from
ftp://replicant.csci.unt.edu/pub/oleg/c++advio.shar
ftp://replicant.csci.unt.edu/pub/oleg/c++advio.cpt.hqx (Mac version)
The Mac version is identical to the UNIX version, but includes CW projects
and a compiled library (for a PowerMac).
***** Verification files: vimage vrectangle vimage_io vfilter
vmorph_filter vfractal_map
Don't forget to compile and run them, see comments in the Makefile for
details. The verification code checks to see that all the functions
in this package have compiled and run well. The code can also serve as
an example how package classes/functions can be used. Sample test
pictures are not distributed with the .shar archive to save space. So,
to run vimage_io.cc, you need to make a subdirectory 'pictures' and
put there any pgm/xwd/tiff file you'd like to be used in the test
(name the file as 512.xwd though, or modify vimage_io.cc). At any
rate, you can get a sample file from
ftp://replicant.csci.unt.edu/pub/oleg/512.xwd.gz
***** Highlights and idioms
Elementary pixel operations: assigning/subtracting/adding etc a value
to all pixels, comparing every pixel with a value,
assigning/adding/subtracting/comparing two images, computing image
extrema, various norms and "scalar" products
IMAGE im1(256,256,8); IMAGE im2(im1);
im1 = 1; im2 = im1; im1 *= 4;
assert( im1 == 4 ); assert( im1 > 0 );
im1.invert(); im1.clear(); im2 <<= 2; im1 = 5;
im1 &= 0xfe; assert( im1==im2 );
im1.clear(); im1 -= im2; im2 = im1;
assert( im1 * im1 == norm_2_sqr(im1) );
Accessing square or rectangular parts of an image without much fuss
(and without moving a lot of stuff around)
im1 = 2*pattern; im2 = pattern;
rowcol rightbottom(im1.q_nrows()-1,im1.q_ncols()-1);
rowcol center(im1.q_nrows()/2,im1.q_ncols()/2);
// Modifying the pixels only within
// the lower left quadrant
im1.rectangle(center,rightbottom) -= 2*pattern;
assert( !(im1 == 2*pattern) && !(im1 != 2*pattern) );
Image i/o: supports reading and writing PGM, XWD and Group G
(grayscale) TIFF file formats with automatical recognition of the
input image file format
// Note the "extended" file name
IMAGE raw_image("zcat ../pictures/lenna.tiff.Z |");
IMAGE image = raw_image.square_of(256,rowcol(120,100));
image.write_tiff("/tmp/ior","Original image");
image.display("Image to display");
image.write_pgm("| xv -"); // another way to display an image
Squeezing/stretching and coercing images. Coercing means that one can
assign one image to another no matter what their dimensions are. The
source image would be shrunk/stretched to fit. Any dimension ratios
are possible, absolutely any
Test_image = pattern;
IMAGE blown_out(IMAGE::Expand,Test_image);
IMAGE blown_shrunk(IMAGE::Shrink,blown_out);
assert( blown_shrunk == Test_image );
IMAGE shrunk(Test_image.q_nrows()/3+1,
Test_image.q_ncols()/2,Test_image.q_depth());
shrunk.coerce(Test_image);
IMAGE vert_stretched(Test_image.q_nrows()+7,
Test_image.q_ncols(),Test_image.q_depth());
vert_stretched.coerce(Test_image);
assert( vert_stretched.rectangle(rowcol(0,0),
rowcol(Test_image.q_nrows()-1,
Test_image.q_ncols()-1))
== Test_image );
Note that the last operation involves an implicit conversion from a
rectangle to an image
Advanced pixel operations via PixelAction. This is the most natural
(and efficient!) way of doing a "sweeping" image processing (that is,
an operation that is going to involve any pixel in some systematic
way). Thus, instead of writing a loop over all image rows and
columns, one merely needs to specify what action is to be performed on
a current pixel. The package would take care of the iteration, which
is more efficient than a for() loop. The iteration walks through all
the pixels in a row-by-row fashion; one can use this knowledge if one
wishes to. PixelAction would tell you the location of the pixel being
accessed/modified, while PixelPrimAction won't (if it doesn't
matter). The latter is faster, of course.
For example, the following snippet squares all image pixels and
verifies that
{
IMAGE im(Test_image); IMAGE im1(im);
im = pattern;
im.square_of(size,rowcol(0,0)) = -1;
im1 = im;
struct SqrImage : public PixelPrimAction {
void operation(GRAY& pixel) { pixel = sqr((GRAY_SIGNED)pixel); }
};
im1.apply(SqrImage());
assert( sum_over((Rectangle)im1) == im.norm_2_sqr() );
}
Still, lookup tables are better for that purpose (see below). In the
next example, which makes a pin-striped image, the location of the
current pixel _is_ important:
class MakeVStripes : public PixelAction
{
GRAY pattern;
void operation(GRAY& pixel) { pixel = col & 1 ? pattern : 0; }
public: MakeVStripes(const GRAY _p) : pattern(_p) {}
};
Test_image.apply(MakeVStripes(pattern));
Finally, the following snippet of the verification code checks out to
see that the pixel actions are indeed executed row-by-row. One
iterator is used to assign to each pixel its own offset from the
beginning of the image, and the other iterator checks it.
{
cout << "Check to see that PixelAction are executed row-wise" << endl;
class assign_pixels : public PixelAction {
const card test_im_nrows, test_im_ncols;
void operation(GRAY& pixel)
{
assert(nrows == test_im_nrows);
assert(ncols == test_im_ncols);
pixel = col + row*ncols;
}
public: assign_pixels(const IMAGE& im) :
test_im_nrows(im.q_nrows()), test_im_ncols(im.q_ncols()) {}
}
Test_image.apply(assign_pixels(Test_image));
class check_pixels : public PixelPrimAction {
GRAY curr_offset;
void operation(GRAY& pixel) { assert(pixel == curr_offset++); }
public: check_pixels(void) : curr_offset(0) {}
}
Test_image.apply(check_pixels());
}
Lazy images: instead of returning an object return a "recipe" how to
make it. The full image would be rolled out only when and where it's
needed:
IMAGE map = FractalMap(order);
FractalMap is a *class*, not a simple function. However similar this
looks to a returning of an object, it's dramatically
different. FractalMap() constructs a LazyImage, an object of just a
few bytes long. A special "IMAGE(const LazyImage& recipe)" constructor
follows the recipe and makes the fractal map right in place. No pixel
is moved whatsoever!
Since the FractalMap is a class, it can be subclassed to modify the
default behavior (say, to override the default uniform noise generator
with a gaussian noise generator, which tends to produce better looking
clouds)
class GaussNoise : public FractalMap
{
public:
GaussNoise(const card order, const Seeds& seeds,
const bits_per_pixel=8)
: FractalMap(order,seeds,bits_per_pixel) {}
inline int get_noise(const card scale) const {
long sum = 0;
for(register int i=0; i<12; i++)
sum += rand(); // keep the result within
return (scale * (sum-(6<<15)))>>17; } // [-scale/2,scale/2]
};
IMAGE map = type == 0 ?
(LazyImage&)FractalMap(order,FractalMap::Seeds(sul,sll,sur,slr)) :
(LazyImage&)GaussNoise(order,FractalMap::Seeds(sul,sll,sur,slr));
This technique is particularly useful when one needs to construct an
image in some particular way (say, by reading it from a file/database,
or by decoding/decompressing, etc) and return it. Thus, instead of
returning an image, one should always return a LazyImage.
cout << "\tExpansion of the uniform image with a small stain\n";
Test_image = pattern;
Test_image(0,0) = 1;
Test_image(1,1) = 0;
IMAGE blown_out(IMAGE::Expand,Test_image);
class BlowImage : public LazyImage
{
const IMAGE& orig_image;
void fill_in(IMAGE& im) const
{
for(register int i=0; i<im.q_nrows(); i++)
for(register int j=0; j<im.q_ncols(); j++)
im(i,j) = orig_image(i/2,j/2);
}
public:
BlowImage(const IMAGE& image) :
LazyImage(2*image.q_nrows(),2*image.q_ncols(),image.q_depth()),
orig_image(image) {}
};
IMAGE another_blown_out = BlowImage(Test_image);
assert( another_blown_out == blown_out );
Image filtration. The package supports a whole bunch of various image
filtration techniques: convolutional, median, and morphological. They
are *very* optimized, and *very* fast. All filtration is done
in-place. My experience tells that it takes noticeably more time to
read an image than to filter it. It's possible to apply a
convolution/median filter to rows only, or to columns only, or to both
rows and columns. A convolution kernel can have either int or
rational coefficients; in the latter case, the denominator can be
specified as an exact power of two, or as just any integer. Note, for
a computer, a floating point number is the rational
number. Convolution algorithms are optimized to all these particular
cases (and to the generic case, too).
Test_image = vert_line;
verify_identity(FilterIt(Test_image).
conv(conv_kernel(1,2,1),FilterIt::Columns)>>=2,vert_line);
verify_identity(FilterIt(Test_image).
conv_col(conv_kernel(1,2,3,CommonDenom(6))),vert_line);
verify_identity(FilterIt(Test_image).
conv(conv_kernel(0,2,0,over_2_up(1))),vert_line);
expected = 0;
expected.rectangle(rowcol(0,vert_line.q_ncols()/2-1),
rowcol(vert_line.q_nrows()-1,
vert_line.q_ncols()/2+1)) = -seed;
verify_identity(FilterIt(Test_image=vert_line).
conv(conv_kernel(1,1,1),FilterIt::Rows),expected);
The following (intentionally contrived) snippet from the verification
code performs a phase shift of an image through filtration
expected = small_sq(0,0);
expected.square_of(2,rowcol(sq_row-1,sq_col-1)) =
small_sq.square_of(2,rowcol(sq_row,sq_col));
verify_identity(FilterIt(Test_image=small_sq).
conv(conv_kernel(0,0,2,over_2_up(1))),expected);
The package supports median filtration with window sizes of three and
five. The filtration can be done by rows only, by columns only, or by
rows _and_ by columns. The latter is equivalent to a 2D median
filtration with a diamond-shaped window.
verify_identity(FilterIt(Test_image).median(FilterIt::RowsAndColumns,3),
expected);
verify_identity(FilterIt(Test_image).median(FilterIt::RowsAndColumns,5),
expected);
Lookup table substitutions are very powerful and very fast. Indeed, in
a typical 512x512 8-bit deep grayscale picture, there are 1/4 M pixels
but only 256 possible pixel values. Therefore, if one needs to perform
some pixel calculation
new_pixel(i,j) = f(old_pixel(i,j))
the fastest way of accomplishing this task is to create a look-up table,
fill it in like lookup(i) = f(i), i=0..255, and then do
FilterIt(image).translate(lookup,LookupT::CoerceFringes);
to substitute all "old pixels" of an 'image' with the "new_pixel"
values. The second argument, LookupT::CoerceFringes or
LookupT::LeaveFringes determines what to do with the pixels (if any)
that fall outside of the lookup table range. Note, a lookup table can
have any number of entries, even 1.
The following example changes all pixels with value 2 to value 3:
LookupT map(LookupT::MapTo(2,3));
assert( FilterIt(Test_image=diverse_image).
translate(map,LookupT::LeaveFringes) != 2 );
(the assert statement makes sure that there is no pixel with value 2
left in the translated image).
a slight modification
verify_pixel_value(FilterIt(Test_image=diverse_image).
translate(map,LookupT::CoerceFringes),3);
makes all pixels have the same value, three. Aren't lookup tables
powerful, or what?
There are many ways to create lookup tables: allocate a blank table
and fill it in, create an Identity lookup table for a particular image
depth and modify a few entries, or perform a composition of existing
lookup tables.
Note
FilterIt(image).translate(lookup,LookupT::CoerceFringes);
returns a reference to an image after the substitution, so one can
use it in chains like
assert( FilterIt(Test_image=diverse_image).
translate(map,LookupT::LeaveFringes) != seed );
***** Grand plans
- a PixelAction-like class for lookup table elements
- in PixelAction, a method 'operation(GRAY& pixel)' should return bool
(if it returns false, the traversal is terminated). Maybe there should
be another class, like PixelControlledAction (which also would have
specifications as to where to start and where/when to end the traversal),
OR traversal classes for Rectangles
- make operation() method of PixelAction non-virtual, and make IMAGE::apply()
templated to a PixelAction-like class (when gcc starts supporting member
function templates)
- make IMAGE::allocate() virtual, or define a class ImageData that contains
only the pixel matrix without indices. So when one needs to handle an
array of images (frames or color-separation planes), the memory won't be
wasted on many (mostly redundant) indices
- add spans (by generalizing rowcol's), 1D, 2D spans, maybe even regions (non-
rectangular spans). Add IMAGE constructors from spans. Use spans
as generalized indices to take slices of an image
- add a LazyMask to achieve
B(A==5) = 4;
(that is, for all i,j; if(A(i,j) ==5) B(i,j) = 4; ), like in Matlab.
- simple color imagery with transformations among various tristimulus
representations
- support addition of a rectangle to an image or an image to a rectangle,
or comparison between a rectangle and an image
- add convolutions with a 5-point kernel and with 3x3 (non-separable) kernels
- add a "non-deterministic averaging" filtering (like in LIFE, only
survival of a pixel is probabilistic, and the chance of survival depends
on how much a pixel sticks out of its neighborhood)
***** Revision history
version 2.0
Makefile is much more user-friendly.
Introduced card (typedef'ed as unsigned int) for row and col dimensions,
indices and other quantities which are always non-negative.
Added PixelAction and PixelPrimAction, classes to do a specific (maybe
very complex) operation on every pixel regardless of its position
(PixelPrimAction) or with regard to its position (PixelAction). The image
is traversed row-by-row.
Introduced LazyImage (which is what to return instead of IMAGE).
bool is used when appropriate.
Removed GNU extensions, code is made very portable.
Sundry of optimizations.
IMAGE::operator(): now it returns GRAY (the pixel itself) in const contexts,
otherwise, it returns a reference to a pixel.
read_xwd/read_pgm use "new style" with the PixelAction stuff.
Median filtration re-worked; can be done now only by rows, only by cols,
and both by Rows and columns. It's heavily optimized and must be very fast.
Added convolutions: by rows, by cols, and separable 2D. Very efficient:
kernel coeffs can be int, rational with power2 common denom, and
rational. The three cases are handled separately (yet uniformly) and very
efficiently (almost as much as one can get). Filtering is done "in-place"
Added assignments with stretching/squeezing an image to fit (IMAGE::coerce()),
the stretching/squeezing is done by arbitrary ratio! (the sizes of the
original and the assigned image can be anything, not necessarily int).
Added lookup table translations.
version 1.15, Feb 8, 1995
Cosmetic changes to please gcc 2.6.3 and adjust to the new version
of endian_io.h
version 1.14, Mar 24, 1994 (previously posted on comp.sources.misc)
Added support for reading/writing PGM and TIFF image file formats
(in addition to the existed support for XWD format). Default write
is in XWD format.
Added image histogram equalization.
Generalizing the Square_area class to the Rectangle class, which handles
arbitrary rectangular areas of the image.
Added comparison predicates that check a relation between an int and all
pixels of the image.
Added Abs() for putting down negative pixels, and application of a generic
user-supplied function to every pixel of the image.
Added +, -, *, >>, and << operations on rowcol (returning rowcol)
Added assignments, comparison, and arithmetics (offsetting) on objects of
the class rowcol.
Added finding Extrema pixel values and image normalization for display.
version 1.1, Apr 3, 1992
Initial revision